package net.jueb.httpFileServer;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.regex.Pattern;

import javax.activation.MimetypesFileTypeMap;

import org.apache.log4j.Logger;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelProgressiveFuture;
import io.netty.channel.ChannelProgressiveFutureListener;
import io.netty.channel.DefaultFileRegion;
import io.netty.channel.FileRegion;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.stream.ChunkedFile;
import io.netty.util.CharsetUtil;

/**
 * 一个简单的业务处理类，只在收到消息后进行业务逻辑处理
 * @author helin
 *
 */
public class HttpFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest>{
	private static Logger log=Logger.getLogger(HttpFileServerHandler.class);
	private  String rootDir;//本地文件根目录
	
	public HttpFileServerHandler(String rootDir) {
		this.rootDir=rootDir;
	}
	
	@Override
	protected void messageReceived(ChannelHandlerContext ctx,FullHttpRequest request) throws Exception 
	{
		if (!request.getDecoderResult().isSuccess()) 
		{//如果解码错误，则返回错误的请求
		    sendError(ctx, HttpResponseStatus.BAD_REQUEST);
		    return;
		}
		if (request.getMethod() != HttpMethod.GET) 
		{//如果请求方法错误
		    sendError(ctx, HttpResponseStatus.METHOD_NOT_ALLOWED);
		    return;
		}
		String uri = request.getUri();//获取请求标识 
		/**
		 * 比如
		 * http://127.0.0.1/index.html 	==>/index.html
		 * http://127.0.0.1/a 			==>/a
		 */
		final String localPath = uriToLocalPath(uri,rootDir);//转换为filePath
		log.debug("对应本地路径："+localPath);
		if (localPath == null) 
		{//如果文件不存在
		    sendError(ctx, HttpResponseStatus.FORBIDDEN);
		    return;
		}
		File localFile = new File(localPath);
		if (!localFile.exists()) 
		{//如果文件或目录不存在
		    sendError(ctx, HttpResponseStatus.NOT_FOUND);
		    return;
		}
		
		if (localFile.isDirectory()) 
		{//如果是目录
			log.debug("查看本地目录："+localFile.getAbsolutePath());
			sendViewDir(ctx, localFile,uri);
		    return;
		}else
		{//如果是文件
			log.debug("查看本地文件："+localFile.getAbsolutePath());
			viewFile(ctx, request, localFile);
		}
	}
	
	/**
	 * 展示文件//传输文件
	 * @param ctx
	 * @param request
	 */
	private void viewFile(ChannelHandlerContext ctx,FullHttpRequest request,File localFile)throws Exception 
	{
		RandomAccessFile randomAccessFile = null;
		try {
		    randomAccessFile = new RandomAccessFile(localFile, "r");// 以只读的方式打开文件
		} catch (FileNotFoundException fnfe) {
		    sendError(ctx, HttpResponseStatus.NOT_FOUND);
		    return;
		}
		long fileLength = randomAccessFile.length();//获取文件大小
		//定义一个默认http响应
		HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
		response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, fileLength);//设置内容长度
		setContentTypeHeader(response, localFile);//设置内容类型
		if (HttpHeaders.isKeepAlive(request)) 
		{//如果是保持连接，则设置属性
		    response.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
		}
		ctx.write(response);//因为加了编码器，所以直接写响应对象
		ChannelFuture sendFileFuture;
		
		/**
		 * 大文件基于块大小传输
		 */
//		log.debug("ChunkedFile方式读取文件并传输！");
//		ChunkedFile chunkFile=new ChunkedFile(randomAccessFile, 0,fileLength, 8192);
//		sendFileFuture = ctx.write(chunkFile, ctx.newProgressivePromise());
		
		/**
		 * 异步文件读取
		 */
		log.debug("DefaultFileRegion方式读取文件并传输！");
		FileRegion dfr=new DefaultFileRegion(randomAccessFile.getChannel(), 0, fileLength);
		sendFileFuture = ctx.write(dfr, ctx.newProgressivePromise());
		
		//添加写监听器
		sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
		    /**
		     * 操作过程
		     * @param future
		     * @param progress
		     * @param total
		     */
			@Override
		    public void operationProgressed(ChannelProgressiveFuture future,long progress, long total) 
			    {
					if (total < 0) 
					{ // total unknown
						log.debug("Transfer progress: " + progress);
					} else 
					{
						log.debug("Transfer progress: " + progress + " / "+ total);
					}
			    }
			/**
			 * 操作完成
			 * @param future
			 * @throws Exception
			 */
		    @Override
		    public void operationComplete(ChannelProgressiveFuture future)throws Exception 
		    {
		    	log.debug("Transfer complete.");
		    }
		});
		ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
		if (!HttpHeaders.isKeepAlive(request)) 
		{//如果不是保持连接，则关闭
		    lastContentFuture.addListener(ChannelFutureListener.CLOSE);
		}
	}

	   
	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)throws Exception 
	{
		if (ctx.channel().isActive()) 
		{
		    sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR);
		}else
		{
			ctx.close();
		}
	}
	
	/**
	 * 应该是转换请求资源到文件路径
	 * @param uri
	 * @return
	 */
	private static String uriToLocalPath(String uri,String rootDir) {
		try {
		    uri = URLDecoder.decode(uri, "UTF-8");
		} catch (UnsupportedEncodingException e) {
		    try {
			uri = URLDecoder.decode(uri, "ISO-8859-1");
		    } catch (UnsupportedEncodingException e1) {
			throw new Error();
		    }
		}
		log.debug("请求资源："+uri);
		if(uri.equals("/"))
		{//如果是根目录，则返回
			return rootDir;
		}else
		{
			return rootDir+uri.substring(1);
		}
	}
	
	/**
	 * 将某个文件或者目录展示给客户端
	 * @param ctx
	 * @param file
	 * @param uri 当前合法的uri请求 注意，这个URL是没有解码的，中文还是乱码
	 */
	private void sendViewDir(ChannelHandlerContext ctx,File dir,String uri)
	{
		log.debug("当前查看列表uri："+uri);
		try {
		    uri = URLDecoder.decode(uri, "UTF-8");
		} catch (UnsupportedEncodingException e) {
		    try {
			uri = URLDecoder.decode(uri, "ISO-8859-1");
		    } catch (UnsupportedEncodingException e1) {
			throw new Error();
		    }
		}
		log.debug("解码后的uri："+uri);
		
		FullHttpResponse response=new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,HttpResponseStatus.OK);
		response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "text/html;charset=UTF-8");
		
		ByteBuf buffer = Unpooled.copiedBuffer(buildHtmlContent(dir, uri), CharsetUtil.UTF_8);
		response.content().writeBytes(buffer);
		buffer.release();//获取剩余字节
		ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
	}
	
	/**
	 * 构建html页面内容
	 * @param dir 本地目录
	 * @param uri 与目录对应的请求uri
	 * @return
	 */
	private static StringBuffer buildHtmlContent(File dir,String uri)
	{
		SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd hh24:mm:ss");
		MimetypesFileTypeMap mimeTypesMap=new MimetypesFileTypeMap();
		StringBuffer sb=new StringBuffer();
		sb.append("<html>\r\n");
		sb.append("<head><title>HttpFileServer</title></head>\r\n");
		sb.append("<body bgcolor=\""+"white\">\r\n");
		sb.append("</h1>\r\n");
		//构建目录路径
		String[] dirs=uri.split("/");
		if(dirs.length==0)
		{//根目录
			/**
			 * 当前查看列表uri：/
			 * []
			 */
			sb.append("<a href=\"/\">根目录</a>/");//文件名
		}else
		{
			/**
			 * 当前查看列表uri：/Bos/Jueb/
			 * [, Bos, Jueb]
			 */
			StringBuffer tmp=new StringBuffer();
			for(int i=0;i<dirs.length;i++)
			{
				if(dirs[i].trim().length()==0)
				{//是第一个根目录
					tmp.append("/");
					sb.append("<a href=\""+tmp.toString()+"\">根目录</a>/");//文件名
				}else
				{
					tmp.append(dirs[i].trim()+"/");
					sb.append("<a href=\""+tmp.toString()+"\">"+dirs[i].trim()+"</a>/");//文件名
				}
			}
		}
		
		sb.append("</h1><hr>\r\n");
		
		//构建table文件列表
		sb.append("<table border=\""+"1\">"+"\r\n");
		sb.append("<thead>"+"\r\n");
		sb.append("<tr>"+"\r\n");
		sb.append("<th>当前目录</th>"+"\r\n");
		sb.append("<th>文件类型</th>"+"\r\n");     
		sb.append("<th>大小</th>"+"\r\n");  
		sb.append("<th>修改时间</th>"+"\r\n");
		sb.append("</tr>"+"\r\n");
		sb.append("</thead>"+"\r\n");//表头结束
		sb.append("<tbody>"+"\r\n");
		for(File f:dir.listFiles())
		{//构建一个文件信息table行
			sb.append("<tr>"+"\r\n");
			if(f.isDirectory())
			{//如果是目录
				sb.append("<td><a href=\""+f.getName()+"/\">"+f.getName()+"/</a></td>"+"\r\n");//文件名
				sb.append("<td>目录</td>"+"\r\n");//文件类型
				sb.append("<td>null</td>"+"\r\n");//文件大小
				sb.append("<td>"+sdf.format(new Date(f.lastModified()))+"</td>"+"\r\n");//修改时间
			}else
			{
				sb.append("<td><a href=\""+f.getName()+"\">"+f.getName()+"</a></td>"+"\r\n");//文件名
				sb.append("<td>"+mimeTypesMap.getContentType(f)+"</td>"+"\r\n");//文件类型
				sb.append("<td>"+f.length()/1024+"kb</td>"+"\r\n");//文件大小
				sb.append("<td>"+sdf.format(new Date(f.lastModified()))+"</td>"+"\r\n");//修改时间
			}
			sb.append("</tr>"+"\r\n");
		}
		sb.append("</tbody>"+"\r\n");
		sb.append("</table>"+"\r\n");
		sb.append("<hr>"+"\r\n");
		sb.append("</body>"+"\r\n");
		sb.append("</html>"+"\r\n");
		return sb;
	}
	
	
	/**
	 * 返回转发回应给客户端
	 * @param ctx
	 * @param newUri
	 */
	private static void sendRedirect(ChannelHandlerContext ctx,String newUri)
	{
		FullHttpResponse response=new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,HttpResponseStatus.FOUND);
		response.headers().set(HttpHeaders.Names.LOCATION,newUri);
		ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
	}
	
	/**
	 * 根据状态反馈信息给客户端
	 * @param ctx
	 * @param status
	 */
	private static void sendError(ChannelHandlerContext ctx,HttpResponseStatus status)
	{
		String result="错误! 状态码："+status+"\r\n";
		ByteBuf content=Unpooled.copiedBuffer(result,CharsetUtil.UTF_8);
		FullHttpResponse response=new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,status,content);
		response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "text/plain; charset=UTF-8");
		ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
	}
	/**
	 * 根据文件设置返回内容类型
	 * @param response
	 * @param file
	 */
	private static void setContentTypeHeader(HttpResponse response,File file)
	{
		MimetypesFileTypeMap mimeTypesMap=new MimetypesFileTypeMap();
		response.headers().set(HttpHeaders.Names.CONTENT_TYPE,mimeTypesMap.getContentType(file));
	}
}
